package be.ac.ulb.mlg.utils.renormalizer;

/*
 * The MIT License (MIT)
 * 
 * Copyright (c) 2013 Jean-Sebastien Lerat (Jean-Sebastien.Lerat@ulb.ac.be)
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * 
 * @author Jean-Sebastien Lerat (Jean-Sebastien.Lerat@ulb.ac.be)
 * @version 1.00, 24/06/2013
 */

import static org.junit.Assert.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import be.ac.ulb.mlg.utils.DefaultRenormalizer;
import be.ac.ulb.mlg.utils.Measure;
import be.ac.ulb.mlg.utils.TestUtils;
import be.ac.ulb.mlg.utils.TestUtils.DataSize;

public class TaxonRenormalizerTest {
	private static final String
		HIGHEST_A	= "HA",
		HIGHEST_B	= "Hb",
		MIDDLE_A1	= "mA1",
		MIDDLE_A2	= "mA2",
		LOW_A11		= "La11",
		LOW_A12		= "La12",
		LOW_A21		= "LA21",
		LOW_B		= "Lb",
		LOW_HIGH_C	= "C"
	;
	private static final String[] TAXON = {
		constructTaxaString(new String[]{HIGHEST_A,MIDDLE_A1,LOW_A11}),
		constructTaxaString(new String[]{HIGHEST_A,MIDDLE_A1,LOW_A12}),
		constructTaxaString(new String[]{HIGHEST_A,MIDDLE_A2,LOW_A21}),
		constructTaxaString(new String[]{HIGHEST_B,LOW_B}),
		LOW_HIGH_C
	};
	
	private final double[][] inputBefore;
	private double[][] inputAfter;
	private TaxonRenormalizer normalizer;
	
	public TaxonRenormalizerTest(){
		inputBefore = TestUtils.generateData(DataSize.SMALL,1e2,0d);
	}
	
	@Before
	public void setUp() throws Exception {
		normalizer = new TaxonRenormalizer();
		for(String taxa:TAXON)
			normalizer.addTaxa(taxa);
	}

	@After
	public void tearDown() throws Exception {
	}
	
	@Test
	public void testReset() throws Exception{
		//no taxa
		String[] taxas = defaultTaxa();
		this.normalizer.setTaxa(taxas);
		
		this.normalizer.reset();
		
		try{
			this.normalizer.setTaxa(taxas);
			fail("Don't clear the taxon definition");
		}catch(NullPointerException npe){
			//nothing to do because we need this exception
		}
		
		for(String taxa:taxas)
			try{
				normalizer.getTaxaIdentifier(taxa);
				fail("Reset taxa fails because the associated DI are still available");
			}catch(NullPointerException npe){
				//nothing to do because we need this exception
			}
		
		// no specific renorms
		setUp();
		Arrays.fill(taxas, LOW_HIGH_C);
		normalizer.setTaxa(taxas);
		testHelper();
		double[][] backUp = this.inputAfter;
		for(int col=0;col<this.inputBefore.length;col++)
			normalizer.setNormalizer(col, 1.0);
		
		normalizer.reset();
		{//set up the "same configuration" without redefining divider (test momory cleaning)
			setUp();
			Arrays.fill(taxas, LOW_HIGH_C);
			normalizer.setTaxa(taxas);
		}
		
		testHelper();
		assertTrue("Reset specific renorms fails",TestUtils.equals(backUp, this.inputAfter));

		// no more results
		setUp();
		Arrays.fill(taxas, LOW_HIGH_C);
		normalizer.setTaxa(taxas);
		testHelper();
		
		normalizer.reset();
		
		int[] indices	= normalizer.getIndexResult();
		String[] named	= normalizer.getNamedResult();
		
		assertNull("The getIndexResult() method must return a null value", indices);
		assertNull("The getNamedResult() method must return a null value", named);
		
		// no more results by groups
		setUp();
		Arrays.fill(taxas, LOW_HIGH_C);
		normalizer.setTaxa(taxas);
		normalizer.setGroups(new int[this.inputBefore.length], 2);//all data into group 0 (auto fill with 0 because Java)
		normalizer.setGroup(0,1);//change one row into another group
	
		testHelper();
		
		normalizer.reset();
		
		indices				= normalizer.getIndexResult();
		named				= normalizer.getNamedResult();
		final int[] groups	= normalizer.getGroupResult();
		
		assertNull("The getIndexResult() (group mode) method must return a null value", indices);
		assertNull("The getNamedResult() (group mode) method must return a null value", named);
		assertNull("The getGroupResult() (group mode) method must return a null value", groups);
	}
	@Test
	public void testSetNormalizer(){
		final double firstDivider	= 1.0;
		final double lastDivider	= 100.0;
		
		this.normalizer.setNormalizer(0,firstDivider);
		for(int i=1;i<inputBefore[0].length-1;i++)
			this.normalizer.setNormalizer(i,DefaultRenormalizer.SUM_NORMILIZER);
		this.normalizer.setNormalizer(inputBefore[0].length-1,lastDivider);
		
		String[] taxas = new String[this.inputBefore.length];
		Arrays.fill(taxas,LOW_HIGH_C);
		this.normalizer.setTaxa(taxas);
		testHelper();
		
		final double[] result = new double[inputBefore[0].length];//auto fill with 0
		for(int i=0;i<inputBefore.length;i++){
			for(int j=0;j<result.length;j++)
				result[j]+=inputBefore[i][j];
		}
		result[0]				/= firstDivider;
		result[result.length-1]	/= lastDivider;
		
		assertTrue("The size of the input array must change to 1 (contains only C)",this.inputAfter.length == 1);
		assertTrue("All data not divied by the correct sum", TestUtils.equals(new double[][]{result},inputAfter));
	}
	private static final String toHighestTaxa(String taxa){
		if(taxa.length()>2)
			taxa = taxa.substring(0, 2);
		return taxa.toUpperCase().replace('L','H');
	}
	@Test
	public void testgetGroupResult(){
		final String[] taxas = defaultTaxa();
		Map<String,List<Integer>> taxaMap = new HashMap<String,List<Integer>>();
		for(int row=0;row<taxas.length;row++){
			final String taxa = toHighestTaxa(taxas[row]);
			if(!taxaMap.containsKey(taxa))
				taxaMap.put(taxa,new ArrayList<Integer>());
			taxaMap.get(taxa).add(new Integer(row));
		}
		for(String key:taxaMap.keySet())
			Collections.sort(taxaMap.get(key));
		this.normalizer.setTaxa(taxas);
		
		int[] groups = new int[taxas.length];// auto-fill with group 0 because Java
		int groupCount = 1;
		for(int row=0;row<taxas.length;row++){
			final List<Integer> ref = taxaMap.get(toHighestTaxa(taxas[row]));
			if(ref.size()>1 && ref.get(0).intValue()!=row)
				groups[row] = groupCount++;
		}
		this.normalizer.setGroups(groups, groupCount);
		
		//for each data, we remove the normalization => divide by 1
		for(int group=0;group<groupCount;group++)
			for(int column=0;column<this.inputBefore[0].length;column++)
				normalizer.setNormalizer(group, column, 1.0);
		
		testHelper();
		
		final int[] excpectedGroups	= Arrays.copyOf(groups,groups.length);
		final int[] actualGroups	= normalizer.getGroupResult();
		Arrays.sort(excpectedGroups);
		Arrays.sort(actualGroups);
		
		final String[] excpectedTaxa= Arrays.copyOf(taxas,taxas.length);
		for(int i=0;i<excpectedTaxa.length;i++) excpectedTaxa[i] = toHighestTaxa(excpectedTaxa[i]);
		final String[] actualTaxa	= normalizer.getNamedResult();
		Arrays.sort(excpectedTaxa);
		Arrays.sort(actualTaxa);
		
		assertTrue("All groups must be represented as the initial inputs (the index of each data can be swapped according to other results values)",Arrays.equals(excpectedGroups,actualGroups));
		assertTrue("All taxas must be represented as the initial inputs (the index of each data can be swapped according to other results values)",Arrays.equals(excpectedTaxa,actualTaxa));
		
		String[] resultTaxa	= normalizer.getNamedResult();
		int[] resultGroup	= normalizer.getGroupResult();
		
		for(int row=0;row<inputAfter.length;row++){
			int index=-1;
			{//search the corresponding index
				final List<Integer> list = taxaMap.get(resultTaxa[row]);
				for(Integer candidate:list)
					if(resultGroup[row] == groups[candidate.intValue()]){
						if(index < 0) index=candidate.intValue();
						else fail("Error, more than one row by (taxa,group) pair");
					}
			}
			if(index<0)
				fail("Corresponding row not found, normalization process bugs");
			else{
				final double[] actual = inputAfter[row];
				final double[] excpected = inputAfter[row];
				assertTrue("The equivalent row differ from the excpect values (intial)",
						TestUtils.equals(
								new double[][]{excpected},
								new double[][]{actual}
				));
			}
		}
	}
	@Test
	public void testgetNamedResult(){
		final String[] taxas = defaultTaxa();
		
		this.normalizer.setTaxa(taxas);
		testHelper();
		
		String[] named = this.normalizer.getNamedResult();
		Map<String, double[]> excpected = compareResultsHelper(taxas);
		
		assertTrue("Bad highest taxas count",named.length == excpected.size());
		
		for(int index=0;index<named.length;index++)
			assertTrue("The result for taxa "+named[index]+" is inconsistent",TestUtils.equals(
					new double[][]{excpected.get(named[index].toUpperCase())},
					new double[][]{this.inputAfter[index]}
			));
	}
	@Test
	public void testIgnoreMissing(){
		final String[] taxas = new String[this.inputBefore.length];
		this.inputBefore[0][0] = Double.NaN;
		Arrays.fill(taxas, LOW_HIGH_C);
		
		this.normalizer.setTaxa(taxas);
		testHelper();
		
		for(double[] row:inputAfter){
			for(double d:row)
				assertFalse("Results contains NaN value need to ignore them during taxa reduction process",  Double.isNaN(d));
		}
	}
	@Test
	public void testNormalizeInput(){
		//test the addTaxa from this method (add taxa specify the hierarchy)
		final String[] taxas = defaultTaxa();
		
		this.normalizer.setTaxa(taxas);
		testHelper();
		
		Set<String> taxaSet = new HashSet<String>();
		for(String taxa:taxas)
			taxaSet.add(toHighestTaxa(taxa));
	
		Map<String, double[]> excpected = compareResultsHelper(taxas);
		
		assertTrue("Bad highest taxas count",taxaSet.size() == excpected.size());
		
		final String[] names = new String[taxaSet.size()];
		taxaSet.toArray(names);
		Arrays.sort(names);// IN the default values, the default order is A,B,C => Alphabetical
		
		int index=0;
		for(String highest:names){
			assertTrue("The result for taxa "+highest+" is inconsistent",TestUtils.equals(
					new double[][]{excpected.get(highest)},
					new double[][]{this.inputAfter[index]}
			));
			index++;
		}
	}
	@Test
	public void testGetIndexResult(){
		final String[] taxas = defaultTaxa();
		final Map<Integer,String> map = new HashMap<Integer,String>();
		for(String taxon:TAXON){
			String[] taxa = taxon.split("--");
			for(String level:taxa)
				if(!map.containsValue(level))
					map.put(new Integer(this.normalizer.getTaxaIdentifier(level)),level);
		}
		
		this.normalizer.setTaxa(taxas);
		testHelper();
		
		int[] indices = this.normalizer.getIndexResult();
		Map<String, double[]> excpected = compareResultsHelper(taxas);
		
		assertTrue("Bad highest taxas count",indices.length == excpected.size());
		for(int index=0;index<indices.length;index++){
			final String name = map.get(new Integer(indices[index])).toUpperCase();
			assertTrue("The result for taxa "+name+" is inconsistent",TestUtils.equals(
					new double[][]{excpected.get(name)},
					new double[][]{this.inputAfter[index]}
			));
		}
	}
	@Test
	public void testSetTaxa(){
		String[] taxas = defaultTaxa();
		final Map<String,Integer> map = new HashMap<String,Integer>();
		for(String taxon:TAXON){
			String[] taxa = taxon.split("--");
			for(String level:taxa)
				if(!map.containsKey(level))
					map.put(level, new Integer(this.normalizer.getTaxaIdentifier(level)));
		}
		this.normalizer.setTaxa(taxas);
		
		for(int i=0;i<0;i++)
			assertTrue("Taxa consistency",map.get(taxas[i]).intValue() == this.normalizer.getTaxaIdentifier(taxas[i]));
		
		taxas = new String[]{LOW_A11,LOW_B,LOW_HIGH_C};
		double[][] input = new double[Math.min(this.inputBefore.length, taxas.length)][];
		for(int i=0;i<input.length;i++)
			input[i] = Arrays.copyOf(this.inputBefore[i], this.inputBefore[i].length);
		taxas = Arrays.copyOf(taxas, input.length);
		
		this.normalizer.setTaxa(taxas);
		testHelper(input);
		
		double[] sum = new double[input[0].length];
		for(double[] row:input){
			for(int col=0;col<row.length;col++)
				sum[col] += row[col];
		}
		for(double[] row:input){
			for(int col=0;col<row.length;col++)
				row[col] /= sum[col];
		}
		
		assertTrue("The input array must contains sum solumn-wise normalization numbers",TestUtils.equals(input,this.inputAfter));
	}
	@Test
	public void testNormalizeOutput(){
		final double[][] outputBefore	= TestUtils.generateData(DataSize.SMALL,1e2,0d);
		final Measure measure			= null;
		final double[][] inputAfter		= TestUtils.copy(inputBefore);
		final double[][] outputAfter	= TestUtils.copy(outputBefore);
		
		this.normalizer.normalizeOutput(inputAfter, outputAfter, measure);
		
		final boolean outputMustBePreserved = !this.normalizer.processOutput();
		
		assertTrue("Input array change and cannot change",TestUtils.equals(inputBefore,inputAfter));
		assertTrue("Output modification differ from processOutput()",TestUtils.equals(outputBefore,outputAfter)==outputMustBePreserved);
	}
	@Test
	public void testIsHighLevel(){
		final String[] taxas = defaultTaxa();
		final Map<String,Integer> map = new HashMap<String,Integer>();
		for(String taxon:TAXON){
			String[] taxa = taxon.split("--");
			for(String level:taxa)
				if(!map.containsKey(level))
					map.put(level, new Integer(this.normalizer.getTaxaIdentifier(level)));
		}
		
		this.normalizer.setTaxa(taxas);

		final List<String> high = new ArrayList<String>();
		high.add(MIDDLE_A1);
		high.add(MIDDLE_A2);
		
		for(int i=0;i<taxas.length;i++){
			assertTrue("Name/index equality", normalizer.isHighLevel(map.get(taxas[i]).intValue()) == normalizer.isHighLevel(taxas[i]));
			assertTrue("Consistency of the result", normalizer.isHighLevel(taxas[i]) == high.contains(taxas[i]));
		}
	}
	@Test
	public void testIsHighestLevel(){
		final String[] taxas = defaultTaxa();
		final Map<String,Integer> map = new HashMap<String,Integer>();
		for(String taxon:TAXON){
			String[] taxa = taxon.split("--");
			for(String level:taxa)
				if(!map.containsKey(level))
					map.put(level, new Integer(this.normalizer.getTaxaIdentifier(level)));
		}
		
		this.normalizer.setTaxa(taxas);
		
		final List<String> highest = new ArrayList<String>();
		highest.add(HIGHEST_A);
		highest.add(HIGHEST_B);
		highest.add(LOW_HIGH_C);
		
		for(int i=0;i<taxas.length;i++){
			assertTrue("Name/index equality", normalizer.isHighestLevel(map.get(taxas[i]).intValue()) == normalizer.isHighestLevel(taxas[i]));
			assertTrue("Consistency of the result", normalizer.isHighestLevel(taxas[i]) == highest.contains(taxas[i]));
		}
	}
	@Test
	public void testIsLowLevel(){
		final String[] taxas = defaultTaxa();
		final Map<String,Integer> map = new HashMap<String,Integer>();
		for(String taxon:TAXON){
			String[] taxa = taxon.split("--");
			for(String level:taxa)
				if(!map.containsKey(level))
					map.put(level, new Integer(this.normalizer.getTaxaIdentifier(level)));
		}
		
		this.normalizer.setTaxa(taxas);
		
		final List<String> low = new ArrayList<String>();
		low.add(LOW_A11);
		low.add(LOW_A12);
		low.add(LOW_A21);
		low.add(LOW_B);
		low.add(LOW_HIGH_C);
		
		for(int i=0;i<taxas.length;i++){
			assertTrue("Name/index equality", normalizer.isLowLevel(map.get(taxas[i]).intValue()) == normalizer.isLowLevel(taxas[i]));
			assertTrue("Consistency of the result", normalizer.isLowLevel(taxas[i]) == low.contains(taxas[i]));
		}
	}
	@Test
	public void testProcessInput(){
		assertTrue("Input are processed but the class tell it isn't the case",new TaxonRenormalizer().processInput());
	}
	@Test
	public void testProcessOutput(){
		assertFalse("Output are not processed but the class tell it's the case",new TaxonRenormalizer().processOutput());
	}
	private Map<String,double[]> compareResultsHelper(final String[] taxas){
		final String[] highestTaxas = Arrays.copyOf(taxas,taxas.length);
		for(int i=0;i<highestTaxas.length;i++)
			highestTaxas[i] = toHighestTaxa(highestTaxas[i]);
		
		int index=0;
		Set<String> groupsSet = new HashSet<String>();
		for(String highest:highestTaxas)
			groupsSet.add(highest);
		String[] groups = new String[groupsSet.size()];
		Map<String,Integer> groupsID = new HashMap<String,Integer>(groups.length);
		
		for(String groupName:groupsSet){
			groupsID.put(groupName,new Integer(index));
			groups[index++]=groupName;
		}
		DefaultRenormalizer defaultRenorm = new DefaultRenormalizer();
		double[][] values = defaultRenorm.normalizeInput(TestUtils.copy(this.inputBefore));
		double[][] result = new double[groups.length][values[0].length];//autofilled with 0
		for(index=0;index<values.length;index++){
			final int group = groupsID.get(highestTaxas[index]);
			for(int j=0;j<values[index].length;j++)
				result[group][j] += values[index][j];
		}
		Map<String,double[]> map = new HashMap<String,double[]>(result.length);
		for(index=0;index<result.length;index++)
			map.put(groups[index],result[index]);
		return map;
	}
	private void testHelper(){
		testHelper(this.inputBefore);
	}
	private void testHelper(final double[][] input){
		this.inputAfter = TestUtils.copy(input);
		this.inputAfter = this.normalizer.normalizeInput(inputAfter);
	}
	private String[] defaultTaxa(){
		final String[] taxas = new String[this.inputBefore.length];
		final int last		= taxas.length-1;
		final int middle	= last>>1;
		
		for(int i=0;i<middle;i++)
			taxas[i] = LOW_A11;
		for(int i=middle;i<last;i++)
			taxas[i] = LOW_A21;
		taxas[last] = LOW_B;
		return taxas;
	}
	private static String constructTaxaString(String[] taxa){
		StringBuffer buff = new StringBuffer(taxa[0]);
		for(int i=1;i<taxa.length;i++){
			buff.append(TaxonRenormalizer.DEFAULT_SEPARATOR);
			buff.append(taxa[i]);
		}
		return buff.toString();
	}
}
